1 /**
2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This file contains the command line parsing and configuration loading from file.
7 
8 The data flow is to first load the configuration from the file then parse the command line.
9 This allows the user to override the configuration via the CLI.
10 */
11 module code_checker.cli;
12 
13 import std.exception : collectException, ifThrown;
14 import std.typecons : Tuple, Flag;
15 import logger = std.experimental.logger;
16 
17 import code_checker.types : AbsolutePath, Path;
18 
19 @safe:
20 
21 enum AppMode {
22     none,
23     help,
24     helpUnknownCommand,
25     normal,
26     initConfig,
27     dumpConfig,
28 }
29 
30 /// Configuration options only relevant for static code checkers.
31 struct ConfigStaticCode {
32     import code_checker.engine.types : Severity;
33 
34     /// Filter results from analyzers on this severity.
35     Severity severity;
36 }
37 
38 /// Configuration options only relevant for clang-tidy.
39 struct ConfigClangTidy {
40     /// Checks to toggle on/off
41     string[] checks;
42 
43     /// Arguments to be baked into the checks parameter
44     string[] options;
45 
46     /// Argument to the filter parameter
47     string headerFilter;
48 
49     /// Apply fix hints.
50     bool applyFixit;
51 
52     /// Apply fix hints even though they result in errors.
53     bool applyFixitErrors;
54 
55     /// The clang-tidy binary to use.
56     string binary = "clang-tidy";
57 }
58 
59 /// Configuration data for the compile_commands.json
60 struct ConfigCompileDb {
61     import code_checker.compile_db : CompileCommandFilter;
62 
63     /// Command to generate the compile_commands.json
64     string generateDb;
65 
66     /// Raw user input via either config or cli
67     string[] rawDbs;
68 
69     /// Either a path to a compilation database or a directory to search for one in.
70     AbsolutePath[] dbs;
71 
72     /// Do not remove the merged compile_commands.json
73     bool keep;
74 
75     /// Flags the user wants to be automatically removed from the compile_commands.json.
76     CompileCommandFilter flagFilter;
77 }
78 
79 /// Settings for the compiler
80 struct Compiler {
81     /// Additional flags the user wants to add besides those that are in the compile_commands.json.
82     string[] extraFlags;
83 }
84 
85 /// Settings for logging.
86 struct Logging {
87     import code_checker.logger : VerboseMode;
88 
89     VerboseMode verbose;
90 
91     /// If logging to files should be done.
92     bool toFile;
93 
94     /// Directory to log to.
95     AbsolutePath dir;
96 }
97 
98 /// Configuration of how to use the program.
99 struct Config {
100     AppMode mode;
101 
102     ConfigStaticCode staticCode;
103     ConfigClangTidy clangTidy;
104     ConfigCompileDb compileDb;
105     Compiler compiler;
106     MiniConfig miniConf;
107     Logging logg;
108 
109     /// If set then only analyze these files.
110     string[] analyzeFiles;
111 
112     /// Returns: a config object with default values.
113     static Config make() @safe {
114         import code_checker.compile_db : defaultCompilerFlagFilter,
115             CompileCommandFilter;
116 
117         Config c;
118         setClangTidyFromDefault(c);
119         c.compileDb.flagFilter = CompileCommandFilter(defaultCompilerFlagFilter, 1);
120         return c;
121     }
122 
123     string toTOML(Flag!"fullConfig" full) @trusted {
124         import std.algorithm : joiner;
125         import std.ascii : newline;
126         import std.array : appender, array;
127         import std.format : format;
128         import std.utf : toUTF8;
129         import std.traits : EnumMembers;
130         import code_checker.engine : Severity;
131 
132         auto app = appender!(string[])();
133         app.put("[defaults]");
134         app.put(format("# only report issues with a severity >= to this value (%(%s, %))",
135                 [EnumMembers!Severity]));
136         app.put(format(`severity = "%s"`, staticCode.severity));
137         app.put(null);
138 
139         app.put("[compiler]");
140         app.put("# extra flags to pass on to the compiler");
141         app.put(`# extra_flags = [ "-std=c++11", "-Wextra", "-Wdocumentation" ]`);
142         app.put(null);
143 
144         app.put("[compile_commands]");
145         app.put("# command to execute to generate compile_commands.json");
146         app.put(format(`generate_cmd = "%s"`, compileDb.generateDb));
147         app.put("# search for compile_commands.json in this paths");
148         if (compileDb.dbs.length == 0 || compileDb.dbs.length == 1
149                 && compileDb.dbs[0] == Path("./compile_commands.json").AbsolutePath)
150             app.put(format("search_paths = %s", ["./compile_commands.json"]));
151         else
152             app.put(format("search_paths = %s", compileDb.dbs));
153         if (full) {
154             app.put("# flags to remove when analyzing a file in the DB");
155             app.put(format("# filter = [%(%s,\n%)]", compileDb.flagFilter.filter));
156             app.put("# compiler arguments to skip from the beginning. Needed when the first argument is NOT a compiler but rather a wrapper");
157             app.put(format("# skip_compiler_args = %s", compileDb.flagFilter.skipCompilerArgs));
158         }
159         app.put(null);
160 
161         app.put("[clang_tidy]");
162         app.put("# clang-tidy binary to use");
163         app.put(format(`# binary = "%s"`, clangTidy.binary));
164         app.put("# arguments to -header-filter");
165         app.put(format(`header_filter = "%s"`, clangTidy.headerFilter));
166         if (full) {
167             app.put("# checks to use");
168             app.put(format("checks = [%(%s,\n%)]", clangTidy.checks));
169             app.put("# options affecting the checks");
170             app.put(format("options = [%(%s,\n%)]", clangTidy.options));
171         }
172         app.put(null);
173 
174         return app.data.joiner(newline).toUTF8;
175     }
176 }
177 
178 /// Minimal config to setup path to config file and workdir.
179 struct MiniConfig {
180     /// Value from the user via CLI, unmodified.
181     string rawWorkDir;
182 
183     /// Converted to an absolute path.
184     AbsolutePath workDir;
185 
186     /// Value from the user via CLI, unmodified.
187     string rawConfFile = ".code_checker.toml";
188 
189     /// The configuration file that has been loaded
190     AbsolutePath confFile;
191 }
192 
193 /// Returns: minimal config to load settings and setup working directory.
194 MiniConfig parseConfigCLI(string[] args) @trusted nothrow {
195     import std.path : dirName;
196     static import std.getopt;
197 
198     MiniConfig conf;
199 
200     try {
201         std.getopt.getopt(args, std.getopt.config.keepEndOfOptions, std.getopt.config.passThrough,
202                 "workdir", "none not visible to the user", &conf.rawWorkDir,
203                 "c|config", "none not visible to the user", &conf.rawConfFile);
204         conf.confFile = Path(conf.rawConfFile).AbsolutePath;
205         if (conf.rawWorkDir.length == 0) {
206             conf.rawWorkDir = conf.confFile.dirName;
207         }
208         conf.workDir = Path(conf.rawWorkDir).AbsolutePath;
209     } catch (Exception e) {
210         logger.error("Invalid cli values: ", e.msg).collectException;
211         logger.trace(conf).collectException;
212     }
213 
214     return conf;
215 }
216 
217 void parseCLI(string[] args, ref Config conf) @trusted {
218     import std.algorithm : map, among, filter;
219     import std.array : array;
220     import std.format : format;
221     import std.path : dirName, buildPath;
222     import std.traits : EnumMembers;
223     import code_checker.engine.types : Severity;
224     import code_checker.logger : VerboseMode;
225     static import std.getopt;
226 
227     bool verbose_info;
228     bool verbose_trace;
229     std.getopt.GetoptResult help_info;
230     try {
231         string config_file = ".code_checker.toml";
232         string[] compile_dbs;
233         string[] src_filter;
234         string workdir;
235         string logdir = ".";
236         bool dump_conf;
237         bool init_conf;
238 
239         // dfmt off
240         help_info = std.getopt.getopt(args,
241             "clang-tidy-bin", "clang-tidy binary to use", &conf.clangTidy.binary,
242             "clang-tidy-fix", "apply suggested clang-tidy fixes", &conf.clangTidy.applyFixit,
243             "clang-tidy-fix-errors", "apply suggested clang-tidy fixes even if they result in compilation errors", &conf.clangTidy.applyFixitErrors,
244             "compile-db", "path to a compilationi database or where to search for one", &compile_dbs,
245             "c|config", "load configuration (default: .code_checker.toml)", &config_file,
246             "dump-config", "dump the full, detailed configuration used", &dump_conf,
247             "f|file", "if set then analyze only these files (default: all)", &conf.analyzeFiles,
248             "init", "create an initial config to use", &init_conf,
249             "keep-db", "do not remove the merged compile_commands.json when done", &conf.compileDb.keep,
250             "log", "create a logfile for each analyzed file", &conf.logg.toFile,
251             "logdir", "path to create logfiles in (default: .)", &logdir,
252             "severity", format("report issues with a severity >= to this value (default: style) %s", [EnumMembers!Severity]), &conf.staticCode.severity,
253             "vverbose", "verbose mode is set to trace", &verbose_trace,
254             "v|verbose", "verbose mode is set to information", &verbose_info,
255             "workdir", "use this path as the working directory when programs used by analyzers are executed (default: where .code_checker.toml is)", &workdir,
256             );
257         // dfmt on
258         conf.mode = AppMode.normal;
259         if (help_info.helpWanted)
260             conf.mode = AppMode.help;
261         else if (init_conf)
262             conf.mode = AppMode.initConfig;
263         else if (dump_conf)
264             conf.mode = AppMode.dumpConfig;
265         conf.logg.verbose = () {
266             if (verbose_trace)
267                 return VerboseMode.trace;
268             if (verbose_info)
269                 return VerboseMode.info;
270             return VerboseMode.minimal;
271         }();
272 
273         // use a sane default which is to look in the current directory
274         if (compile_dbs.length == 0 && conf.compileDb.dbs.length == 0) {
275             compile_dbs = ["./compile_commands.json"];
276         } else if (compile_dbs.length != 0) {
277             conf.compileDb.rawDbs = compile_dbs;
278         }
279 
280         if (conf.logg.toFile)
281             conf.logg.dir = Path(logdir).AbsolutePath;
282 
283         // dfmt off
284         conf.compileDb.dbs = conf
285             .compileDb.rawDbs
286             .filter!(a => a.length != 0)
287             .map!(a => Path(buildPath(conf.miniConf.workDir, a)).AbsolutePath)
288             .array;
289         // dfmt on
290     } catch (std.getopt.GetOptException e) {
291         // unknown option
292         logger.error(e.msg);
293         conf.mode = AppMode.helpUnknownCommand;
294     } catch (Exception e) {
295         logger.error(e.msg);
296         conf.mode = AppMode.helpUnknownCommand;
297     }
298 
299     void printHelp() @trusted {
300         import std.getopt : defaultGetoptPrinter;
301         import std.format : format;
302         import std.path : baseName;
303 
304         defaultGetoptPrinter(format("usage: %s\n", args[0].baseName), help_info.options);
305     }
306 
307     if (conf.mode.among(AppMode.help, AppMode.helpUnknownCommand)) {
308         printHelp;
309         return;
310     }
311 }
312 
313 /** Load the configuration from file.
314  *
315  * Example of a TOML configuration
316  * ---
317  * [defaults]
318  * check_name_standard = true
319  * ---
320  */
321 void loadConfig(ref Config rval) @trusted {
322     import std.algorithm;
323     import std.array : array;
324     import std.file : exists, readText;
325     import std.path : dirName, buildPath;
326     import toml;
327 
328     if (!exists(rval.miniConf.confFile))
329         return;
330 
331     static auto tryLoading(string configFile) {
332         auto txt = readText(configFile);
333         auto doc = parseTOML(txt);
334         return doc;
335     }
336 
337     TOMLDocument doc;
338     try {
339         doc = tryLoading(rval.miniConf.confFile);
340     } catch (Exception e) {
341         logger.warning("Unable to read the configuration from ", rval.miniConf.confFile);
342         logger.warning(e.msg);
343         return;
344     }
345 
346     alias Fn = void delegate(ref Config c, ref TOMLValue v);
347     Fn[string] callbacks;
348 
349     void defaults__check_name_standard(ref Config c, ref TOMLValue v) {
350         import std.traits : EnumMembers;
351         import code_checker.engine.types : toSeverity, Severity;
352 
353         auto s = toSeverity(v.str);
354         if (s.isNull) {
355             logger.warningf("Unknown severity level %s. Using default: style", v.str);
356             logger.warningf("valid values are: %s", [EnumMembers!Severity]);
357             c.staticCode.severity = Severity.style;
358         } else {
359             c.staticCode.severity = s;
360         }
361     }
362 
363     callbacks["defaults.severity"] = &defaults__check_name_standard;
364 
365     callbacks["compile_commands.search_paths"] = (ref Config c, ref TOMLValue v) {
366         c.compileDb.rawDbs = v.array.map!(a => a.str).array;
367     };
368     callbacks["compile_commands.generate_cmd"] = (ref Config c, ref TOMLValue v) {
369         c.compileDb.generateDb = v.str;
370     };
371     callbacks["compile_commands.filter"] = (ref Config c, ref TOMLValue v) {
372         import code_checker.compile_db : FilterClangFlag;
373 
374         c.compileDb.flagFilter.filter = v.array.map!(a => FilterClangFlag(a.str)).array;
375     };
376     callbacks["compile_commands.skip_compiler_args"] = (ref Config c, ref TOMLValue v) {
377         c.compileDb.flagFilter.skipCompilerArgs = cast(int) v.integer;
378     };
379     callbacks["clang_tidy.header_filter"] = (ref Config c, ref TOMLValue v) {
380         c.clangTidy.headerFilter = v.str;
381     };
382     callbacks["clang_tidy.checks"] = (ref Config c, ref TOMLValue v) {
383         c.clangTidy.checks = v.array.map!(a => a.str).array;
384     };
385     callbacks["clang_tidy.options"] = (ref Config c, ref TOMLValue v) {
386         c.clangTidy.options = v.array.map!(a => a.str).array;
387     };
388     callbacks["compiler.extra_flags"] = (ref Config c, ref TOMLValue v) {
389         c.compiler.extraFlags = v.array.map!(a => a.str).array;
390     };
391 
392     void iterSection(ref Config c, string sectionName) {
393         if (auto section = sectionName in doc) {
394             // specific configuration from section members
395             foreach (k, v; *section) {
396                 if (auto cb = sectionName ~ "." ~ k in callbacks)
397                     (*cb)(c, v);
398                 else
399                     logger.infof("Unknown key '%s' in configuration section '%s'", k, sectionName);
400             }
401         }
402     }
403 
404     iterSection(rval, "defaults");
405     iterSection(rval, "clang_tidy");
406     iterSection(rval, "compile_commands");
407     iterSection(rval, "compiler");
408 }
409 
410 /// Returns: default configuration as embedded in the binary
411 void setClangTidyFromDefault(ref Config c) @safe nothrow {
412     import std.algorithm;
413     import std.array;
414     import std.ascii : newline;
415 
416     static auto readConf(immutable string raw) {
417         // dfmt off
418         return raw
419             .splitter(newline)
420             // remove empty lines
421             .filter!(a => a.length != 0)
422             // remove comments
423             .filter!(a => !a.startsWith("#"))
424             .array;
425         // dfmt on
426     }
427 
428     immutable raw_checks = import("clang_tidy_checks.conf");
429     immutable raw_options = import("clang_tidy_options.conf");
430 
431     c.clangTidy.checks = readConf(raw_checks);
432     c.clangTidy.options = readConf(raw_options);
433     c.clangTidy.headerFilter = ".*";
434 }